package se.webbzon.oschi01.worldeditor;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;

public class WorldAreaReader { 
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Loads the content of a world area into a WorldAreaContent instance
	 * by using an input stream. **/
	public WorldAreaBlueprint load(InputStream inputStream) 
			throws WorldAreaReaderException, IOException {
		return loadWorldAreaBlueprint(new DataInputStream(inputStream));
	}
	
	/** Loads the content of a world area into a WorldAreaContent instance
	 * by using an url. **/
	public WorldAreaBlueprint load(URL url) 
			throws WorldAreaReaderException, IOException {
		URLConnection connection = url.openConnection();
		InputStream inputStream = connection.getInputStream();
		WorldAreaBlueprint blueprint = load(inputStream);
		inputStream.close();
		return blueprint;
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Loads a world area blueprint from a data input stream. **/
	private WorldAreaBlueprint loadWorldAreaBlueprint(DataInputStream dis) 
			throws WorldAreaReaderException, IOException {
		
		// Read the recognizer
		final byte[] recognizer = new byte[4];
		dis.read(recognizer,0,4);
		if (!Arrays.equals(recognizer, new byte[] {'p', 'i', '3', 'e'}))
			throw new WorldAreaReaderException("The file is not a World Area Blueprint!");
			
		// Read version
		final int version = dis.readInt();
		
		// Read the size of the context name (in bytes) 
		final int contextNameSize = dis.readShort();
		
		// Read the number of blueprints in this 3dwa-file
		final int numberOfBlueprints = dis.readInt();
		
		// The the properties of this world area
		final int worldAreaProperties = dis.readInt();
		
		// Read the name of the context
		final byte[] contextNameData = new byte[contextNameSize];
		dis.read(contextNameData,0,contextNameSize);
		final String contextName = new String(contextNameData,"UTF-8");
		
		// Read the world object blueprints
		WorldObjectBlueprint[] blueprints = 
				loadWorldObjectBlueprints(dis,numberOfBlueprints);
		
		// Return the blueprint of the world area
		return new WorldAreaBlueprint(	version,contextName,
										worldAreaProperties,blueprints);
	}
	
	/** Loads all the world object blueprints from a data input stream. **/
	private WorldObjectBlueprint[] loadWorldObjectBlueprints(	DataInputStream dis, 
																int numberOfBlueprints) 
			throws IOException, WorldAreaReaderException {
		// A pointer to the next free world object blueprint position
		int blueprintPtr = 0;
		
		// Create an array of world object blueprints
		final WorldObjectBlueprint[] blueprints = 
				new WorldObjectBlueprint[numberOfBlueprints];
		
		// An id map used to sort the blueprints
		final int[] blueprintIdMap = 
				new int[numberOfBlueprints];

		// A queue used to delay creation of blueprints and check circular dependencies
		final Deque<WorldObjectBlueprint> blueprintDeque = 
				new ArrayDeque<WorldObjectBlueprint>();
				
		// Create all world object blueprints
		for (int blueprint = 0; blueprint < numberOfBlueprints; blueprint++) {
			// Read the header of the blueprint
			final int id = blueprint;
			final int deflationProperties = dis.readInt();
			final int numberOfDependencies = dis.readInt();
			final short deflationNameSize = dis.readShort();
			final int dataSize = dis.readInt();
					
			// Read the dependencies
			final byte[] dependenciesData = new byte[4*numberOfDependencies];
			dis.read(dependenciesData,0,4*numberOfDependencies);
			final IntBuffer buff = ByteBuffer.wrap(dependenciesData).order(ByteOrder.BIG_ENDIAN).asIntBuffer();
			final int[] dependencies = new int[buff.remaining()];
			buff.get(dependencies);
					
			// Read the blueprint name
			final byte[] deflationNameData = new byte[deflationNameSize];
			dis.read(deflationNameData,0,deflationNameSize);
			final String deflationName = new String(deflationNameData,"UTF-8");
					
			// Read the data of the instance
			final byte[] data = new byte[dataSize];
			dis.read(data,0,dataSize);
					
			// Create the world object blueprint
			final WorldObjectBlueprint bp = 
					new WorldObjectBlueprint(	id,deflationName,dependencies,
												deflationProperties,data);
					
			// Check for dependencies
			boolean delay = false;
			for (int dependencyId : dependencies) {
				if (blueprints[dependencyId] == null) {
					delay = true;
					break;
				}
			}
					
			if (delay)
				// Delay creation
				blueprintDeque.add(bp);
			else {
				blueprintIdMap[id] = blueprintPtr;
				blueprints[blueprintPtr++] = bp;
				// Translate the dependency array of the blueprint
				int[] dependencyArray = bp.getDependencies();
				for (int i = 0; i < dependencyArray.length; i++)
					dependencyArray[i] = blueprintIdMap[dependencyArray[i]];
			}
		}
				
		// A help variable used to track circular dependencies
		WorldObjectBlueprint tracker = null;
				
		// Check for circular dependencies in the 3dwa-file
		while (blueprintDeque.size() > 0) {
			WorldObjectBlueprint blueprint = blueprintDeque.remove();
			if (tracker == blueprint)
				throw new WorldAreaReaderException(	"Tracked circular dependencies "+
													"in world object with name \"" +
													blueprint.getName() + "\".");
			// Check for dependencies
			boolean delay = false;
			for (int dependency : blueprint.getDependencies()) {
				if (blueprints[dependency] == null) {
					delay = true;
					break;
				}
			}
					
			if (delay) {
				// Delay until all dependencies exist
				if (tracker == null)
					tracker = blueprint;
				blueprintDeque.add(blueprint);
			} else {
				tracker = null;	
				blueprintIdMap[blueprint.getId()] = blueprintPtr;
				blueprints[blueprintPtr++] = blueprint;
				// Translate the dependency array of the blueprint
				int[] dependencyArray = blueprint.getDependencies();
				for (int i = 0; i < dependencyArray.length; i++)
					dependencyArray[i] = blueprintIdMap[dependencyArray[i]];
			}
		}
		return blueprints;
	}

}
